// Copyright 2010 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview The Layer class represents a layer of information and is
 * designed based on the Chrome Extension and its manifest.  The layer
 * encapsulates background and content scripts that are injected into pages;
 * conditionally for content scripts.  It acts as a unit where no information
 * cross between layers at the management level though they can interact once
 * injected into the same page.  This unit also hides information from the
 * layer manager and only exposes certain data through a public interface.
 *
 * The layer concept also introduces the concept of on-demand loading where
 * scripts loaded from remote sources will be downloaded and injected only
 * when they are needed.  The layer provides the ability to user to set the
 * on-demand attribute on the fly, or set it to never use the on-demand
 * feature.
 *
 * The layer incorporates the idea of state where a layer can be turned on/off
 * by the user, or always on.  Turning a layer on and off changes the
 * interaction of the layer with content by allowing or prohibiting injection
 * of content scripts.  Unfortunately, the current state does not allow the
 * conditional nature of scripts loaded into the background without reloading
 * the entire extension.
 * TODO (jasonstredwick): Add ability to unload/disable layers.
 *
 * Public functions:
 *   shellExtension.Layer() - Constructs a new Layer object that starts in the
 *       BAD_OBJECT state.
 *   shellExtension.Layer.create(id, log) - Create and initialize a new Layer.
 *
 * Public prototype functions for shellExtension.Layer:
 *   clear() - Clears the layer of all non-initialization information including
 *       scripts.
 *   clearData() - Clears all loadable scripts, sets them to INIT, and marks
 *       them as pending.
 *   getScriptDataHandlers(type, log, opt_url) - Get the location (url) and
 *       setter function for each pending script.
 *   getDataStatus() - Returns the status of all managed scripts.
 *   getId() - Returns the id for the layer.
 *   getOndemand() - Returns the on-demand state for the layer.
 *   getState() - Returns the state of the layer; i.e. like on/off.
 *   getStatus() - Returns the status of the layer object, which does not
 *       include the status of its scripts.  See getDataStatus for details on
 *       script status.
 *   initialize(id, log) - Initialize the layer object with an id.
 *       Initialization can only occur on objects with a status of
 *       shellExtension.status.Value.BAD_OBJECT.  The layer is first reset
 *       before initialization occurs.  Also, as the id can come from an
 *       external source it will be validated.
 *   inject(type, log, opt_url, opt_tabId) - Tells the layer inject valid
 *       scripts into a page.  The type input specifies whether injection is
 *       for background or content scripts.
 *   reload(data, log) - Clears the layer and processes new layer data, creates
 *       new resources, and appends them to the appropriate layer variables.
 *       As the data can come from an external source it will be validated.
 *   reset() - Reset the layer object back to its state after creation, so its
 *       status will be shellExtension.status.Value.BAD_OBJECT and have no id.
 *       This differs from clear() as to the degree of reset.
 *   setOndemand(state, log) - Set the on-demand flag for the layer, but only
 *       if the given state is a valid change.  The value can come from an
 *       external source, so it will be validated.
 *   setState(state, log) - Set the state flag for the layer, but only
 *       if the given state is a valid change.  The value can come from an
 *       external source, so it will be validated.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('shellExtension.Layer');

goog.require('bite.common.log.Simple');
goog.require('bite.common.net.url');
goog.require('shellExtension.BackgroundScript');
goog.require('shellExtension.ContentScript');
goog.require('shellExtension.data');
goog.require('shellExtension.manifest.layer');
goog.require('shellExtension.status');



/**
 * Construct a new Layer object.
 * @constructor
 */
shellExtension.Layer = function() {
  /**
   * This array holds all the background scripts in the order loaded from the
   * manifest.  Even though background scripts are held here like content
   * scripts, once injected they can't be manipulated through their object.
   * @type {Array.<!shellExtension.BackgroundScript>}
   * @private
   */
  this.backgroundScripts_ = [];

  /**
   * The base url of the layer.
   * @type {string}
   * @private
   */
  this.baseUrl_ = '';

  /**
   * This array holds all the content scripts in the order loaded from the
   * manifest.
   * @type {Array.<!shellExtension.ContentScript>}
   * @private
   */
  this.contentScripts_ = [];

  /**
   * The id both represents the layer as an identifier and how to retrieve its
   * information remotely.
   * @type {?string}
   * @private
   */
  this.id_ = null;

  /**
   * Used to determine if the layer should be loaded immediately or use the
   * default on-demand loading.
   * @type {shellExtension.manifest.layer.State}
   * @private
   */
  this.ondemand_ = shellExtension.manifest.layer.State.ON;

  /**
   * The on/off state of the layer.
   * @type {shellExtension.manifest.layer.State}
   * @private
   */
  this.state_ = shellExtension.manifest.layer.State.OFF;

  /**
   * Used to record the load status of the layer data not the status of all
   * resources.  They maintain their own status.
   * @type {shellExtension.status.Value}
   * @private
   */
  this.status_ = shellExtension.status.Value.BAD_OBJECT;
};


/**
 * Create a new layer and initialize it.
 * @param {string} id The id for the layer.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @return {!shellExtension.Layer} The new layer.
 */
shellExtension.Layer.create = function(id, log) {
  var layer = new shellExtension.Layer();
  layer.initialize(id, log);
  return layer;
};



/**
 * Clears the layer of all non-initialization information.
 */
shellExtension.Layer.prototype.clear = function() {
  // BAD_OBJECT means that the layer was created but not initialized, so no
  // loading until it is initialized.
  if (this.status_ == shellExtension.status.Value.BAD_OBJECT) {
    return;
  }

  this.backgroundScripts_ = [];
  this.contentScripts_ = [];
  this.status_ = shellExtension.status.Value.INIT;
};


/**
 * Clears the loaded data for all background and content scrips.
 */
shellExtension.Layer.prototype.clearData = function() {
  // Using resources can only occur once the layer has reached the LOADED
  // state.
  if (this.status_ != shellExtension.status.Value.LOADED) {
    return;
  }

  var app = function(data) {
    for (var i = 0, len = data.length; i < len; ++i) {
      data[i].scriptSet.clearData();
    }
  };

  app(this.backgroundScripts_);
  app(this.contentScripts_);
};


/**
 * Composes an array of objects that contain an id and callback for each
 * pending script in the set.  The id corresponds to the location from which
 * the script can be retrieved, and the callback is a setter function for the
 * script that takes a string with the code or null if there is an error.  The
 * format of the set function is function(?string code).  If the script set is
 * in a failed state then return an empty array.
 * @param {shellExtension.data.layer.ScriptType} type The type of layer script.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @param {string=} opt_url An optional url to use when retrieve the handlers.
 * @return {Array.<!{url: string, callback: function(?string)}>} An array of
 *     objects corresponding to pending script.  Each object contains where
 *     to get the script's code and a set function.
 */
shellExtension.Layer.prototype.getScriptDataHandlers = function(type, log,
                                                                opt_url) {
  // Using resources can only occur once the layer has reached the LOADED
  // state.
  if (this.status_ != shellExtension.status.Value.LOADED) {
    return [];
  }

  var url = opt_url || '';
  var isBackground = type == shellExtension.data.layer.ScriptType.BACKGROUND;

  var data = isBackground ? this.backgroundScripts_ : this.contentScripts_;
  var getHandlers = function(script) {
    // If it is a content script and the url does not match then return [];
    if (!isBackground && !script.doesMatch(url)) {
      return [];
    }
    return script.scriptSet.getScriptDataHandlers(log);
  };

  var handlers = [];
  for (var i = 0, len = data.length; i < len; ++i) {
    handlers = handlers.concat(getHandlers(data[i]));
  }

  return handlers;
};


/**
 * Returns the status of the layer's data.
 * @return {shellExtension.status.Value} The status of the layer scripts.
 */
shellExtension.Layer.prototype.getDataStatus = function() {
  var all = this.backgroundScripts_.concat(this.contentScripts_);
  return shellExtension.status.determine(all);
};


/**
 * Returns the id of the layer.
 * @return {?string} The id or null if not initialized.
 */
shellExtension.Layer.prototype.getId = function() {
  return this.id_;
};


/**
 * Returns the on-demand loading flag of the layer.
 * @return {shellExtension.manifest.layer.State} The load flag.
 */
shellExtension.Layer.prototype.getOndemand = function() {
  return this.ondemand_;
};


/**
 * Returns the state of the layer.
 * @return {shellExtension.manifest.layer.State} The state of the layer.
 */
shellExtension.Layer.prototype.getState = function() {
  return this.state_;
};


/**
 * Return the status of the layer.  The status for individual resources are
 * queried independently, see getDataStatus.
 * @return {shellExtension.status.Value} The status of the layer.
 */
shellExtension.Layer.prototype.getStatus = function() {
  return this.status_;
};


/**
 * Initialize the layer object with an id.  Initialization can only occur on
 * objects with a status of shellExtension.status.Value.BAD_OBJECT.  The layer
 * is first reset before initialization occurs.
 * @param {string} id The id of the layer.  As the id can come from an external
 *     source it will be validated.
 * @param {!bite.common.log.Simple} log The log to record errors.
 */
shellExtension.Layer.prototype.initialize = function(id, log) {
  if (this.status_ != shellExtension.status.Value.BAD_OBJECT) {
    return;
  }

  var baseMsg = 'ERROR (shellExtension.Layer.initialize): ';
  if (!shellExtension.manifest.layer.validateId(id)) {
    log.add(baseMsg + 'Invalid id, did not reset.');
    return;
  }

  this.reset();

  // If there were no errors then set the object initialization data.
  this.baseUrl_ = bite.common.net.url.getFullPath(id);
  this.id_ = id;
  this.status_ = shellExtension.status.Value.INIT;
};


/**
 * Loop through all the background scripts, checks that they are loaded, and
 * inject them into the background page.
 * @param {shellExtension.data.layer.ScriptType} type The type of layer script.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @param {string=} opt_url The url to use when retrieve the handlers.
 * @param {number=} opt_tabId The id for the page to inject into.
 */
shellExtension.Layer.prototype.inject = function(type, log, opt_url,
                                                 opt_tabId) {
  // Using resources can only occur once the layer has reached the LOADED
  // state.
  if (this.status_ != shellExtension.status.Value.LOADED) {
    return;
  }

  var baseMsg = 'ERROR (shellExtension.Layer.inject): ';

  var url = opt_url || '';
  var tabId = opt_tabId || -1;
  var isBackground = type == shellExtension.data.layer.ScriptType.BACKGROUND;

  var data = isBackground ? this.backgroundScripts_ : this.contentScripts_;
  var dataName = isBackground ? 'BackgroundScript' : 'ContentScript';
  var inject = function(script, scriptId) {
    if (isBackground) {
      script.inject(scriptId, log);
    } else if (script.doesMatch(url) && tabId >= 0) {
      script.inject(scriptId, log, tabId, url);
    }
  };

  for (var i = 0, len = data.length; i < len; ++i) {
    var script = data[i];

    // Temporary script id.
    var scriptId = this.id_ + ':' + dataName + '[' + i + ']';

    if (script.getStatus() != shellExtension.status.Value.LOADED) {
      log.add(baseMsg + 'Script not loaded (' + scriptId + ').');
      continue;
    }

    inject(script, scriptId);
  }
};


/**
 * Processes an object containing data for the layer, create new resources
 * and append them to the appropriate layer variables.
 * @param {Object} data The data to be processed as a layer. The null value
 *     will cause the layer object to enter the FAILED state.
 * @param {!bite.common.log.Simple} log The log to record errors.
 */
shellExtension.Layer.prototype.reload = function(data, log) {
  var msgBase = 'ERROR (shellExtension.Layer.reload): ';

  // BAD_OBJECT means that the layer was created but not initialized.
  if (this.status_ == shellExtension.status.Value.BAD_OBJECT) {
    log.add(msgBase + 'Layer not initialized.');
    return;
  }

  // The object is coming from an external resource that may not be validated.
  // It can be null and expects the validation function to mark that as an
  // error.
  if (!shellExtension.manifest.layer.validate(data)) {
    this.status_ = shellExtension.status.Value.FAILED;
    log.add(msgBase + 'Invalid layer object.');
    if (data) {
      log.add('Layer data: ' + data);
    }
    return;
  }

  this.clear();

  var state;
  var scripts;

  var property = shellExtension.manifest.layer.Property;
  // Begin processing the raw data and parsel it to the appropriate functions
  // for further processing and object creation.
  for (var propertyKey in property) {
    var key = property[propertyKey];
    if (!(key in data)) {
      continue;
    }

    var dataValue = data[key];

    switch (key) {
      case property.BACKGROUND_SCRIPTS:
      case property.CONTENT_SCRIPTS:
        if (!shellExtension.manifest.layer.validateScriptList(dataValue)) {
          this.status_ = shellExtension.status.Value.FAILED;
          log.add(msgBase + 'Invalid ' + key + '.');
          break;
        }

        dataValue = /** @type {!Array.<!Object>} */ (dataValue);

        var isBackground = key == property.BACKGROUND_SCRIPTS;
        var obj = isBackground ? this.backgroundScripts_ :
                                 this.contentScripts_;
        var create = isBackground ? shellExtension.BackgroundScript.create :
                                    shellExtension.ContentScript.create;
        for (var i = 0, len = dataValue.length; i < len; ++i) {
          obj.push(create(dataValue[i], this.baseUrl_, log));
        }
        break;

      case property.ID:
        // Don't allow the update of this information from this location,
        // so ignoring.
        break;

      case property.ONDEMAND:
      case property.STATE:
        if (!shellExtension.manifest.layer.validateState(dataValue)) {
          this.status_ = shellExtension.status.Value.FAILED;
          log.add(msgBase + 'Invalid ' + key + '.');
          break;
        }

        dataValue =
            /** @type {shellExtension.manifest.layer.State} */ (dataValue);

        if (key == property.ONDEMAND) {
          this.ondemand_ = dataValue;
        } else {
          this.state_ = dataValue;
        }
        break;

      default:
        log.add('WARNING Unknown property in shellExtension.manifest.layer.' +
                'Property (' + key + ').');
        break;
    }
  }

  // In case the status was already failed, don't override that value with
  // LOADED.
  if (this.status_ != shellExtension.status.Value.FAILED) {
    this.status_ = shellExtension.status.Value.LOADED;
  }
};


/**
 * Reset the layer object back to its state after creation, so its status will
 * be shellExtension.status.Value.BAD_OBJECT and have no id.
 */
shellExtension.Layer.prototype.reset = function() {
  this.backgroundScripts_ = [];
  this.baseUrl_ = '';
  this.contentScripts_ = [];
  this.id_ = null;
  this.ondemand_ = shellExtension.manifest.layer.State.ON;
  this.state_ = shellExtension.manifest.layer.State.OFF;
  this.status_ = shellExtension.status.Value.BAD_OBJECT;
};


/**
 * Set the on-demand flag for the layer, but only if the given state is a
 * valid change.
 * @param {shellExtension.manifest.layer.State} state The new flag.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @return {boolean} Whether the set occurred.
 */
shellExtension.Layer.prototype.setOndemand = function(state, log) {
  var msgBase = 'ERROR (shellExtension.Layer.setOndemand): ';
  if (!shellExtension.manifest.layer.validateState(state) ||
      state == shellExtension.manifest.layer.State.FIXED) {
    log.add(msgBase + 'Invalud state (' + state + ').');
    return false;
  }
  this.ondemand_ = state;
  return true;
};


/**
 * Set the state of the layer, but only if the given state is a valid change.
 * @param {shellExtension.manifest.layer.State} state The new state.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @return {boolean} Whether the set occurred.
 */
shellExtension.Layer.prototype.setState = function(state, log) {
  var msgBase = 'ERROR (shellExtension.Layer.setState): ';
  if (!shellExtension.manifest.layer.validateState(state) ||
      state == shellExtension.manifest.layer.State.FIXED) {
    log.add(msgBase + 'Invalud state (' + state + ').');
    return false;
  }
  this.state_ = state;
  return true;
};

